#!/usr/bin/python2
# -*- coding: utf-8 -*-
from eventlet.queue import Empty

from ..topo import Topo,Node,Link

from eventlet import Queue,sleep

import random
import time

from ryuapp.lib.hedera.mock_dep import Operation


class Event:
    def __init__(self):
        pass


class FlowStat:
    def __init__(self, size = 10):
        self.total_size = size # Mb
        self.remained_size = size
        self.speed = 0
        self.path = None


BANDWIDTH = 10.0 # Mbps


class FatTreeSim(Topo):

    def __init__(self, install_queue, reply_queue, fanout=2):
        """
        layer1 1 * 1000 + [0, f**2)
        layer2 2 * 1000 + [0, f**2) * f + [0, f)
        layer3 3 * 1000 + [0, f**2) * f + [0, f)
        :param fanout: fanout<=9
        """
        Topo.__init__(self)
        self.fanout = fanout
        assert isinstance(install_queue,Queue)
        assert isinstance(reply_queue, Queue)
        self.install_queue = install_queue
        self.reply_queue = reply_queue

        self.hosts = {}  # id : ((nodeid,portid), ip)
        self.flows = {}  # (id, id) : FlowStat
        self.unused_host_pairs = []
        self.unused_host_expect_len = 0

        self.flowdonecount = 0
        self.traffic_size = 0

        self.wait_op_list = []
        self.installed_path = {}

        random.seed(100)
        self.__topo_init()
        self.__flow_init()
        self.__stats_update()


    def run(self):
        """
        run without flow simulation
        """
        wait_op_list = []
        while True:
            e = None
            try:
                e = self.install_queue.get(timeout=0.01)
            except Empty:
                pass
            if e is not None:
                sleep(0.01)
                assert isinstance(e, Operation)
                e.endtime = time.time()+e.timeout
                wait_op_list.append(e)
            if wait_op_list:
                now = time.time()
                l = []
                for op in wait_op_list:
                    if now>=op.endtime:
                        self.reply_queue.put(op)
                    else:
                        l.append(op)
                wait_op_list = l

    def __manage_op_list(self):
        if self.wait_op_list:
            now = time.time()
            l = []
            for op in self.wait_op_list:
                if now >= op.endtime:
                    self.reply_queue.put(op)
                    if op.path is not None:
                        self.installed_path[op.pair]=op.path
                        print("install path %s %s"%(op.pair,op.path))
                        if op.pair in self.flows:
                            self.flows[op.pair].path = op.path
                else:
                    l.append(op)
            self.wait_op_list = l
        while not self.install_queue.empty():
            e = self.install_queue.get()
            assert isinstance(e, Operation)
            e.endtime = time.time() + e.timeout
            self.wait_op_list.append(e)


    def __topo_init(self):
        fanout = self.fanout
        for i in range(fanout**2):
            id = 1000 + i
            self.nodes[id] = Node(id)
            for j in range(fanout):
                id = 2000 + i * fanout + j
                self.nodes[id] = Node(id)
                id = 3000 + i * fanout + j
                self.nodes[id] = Node(id)

        for m in range(fanout):
            for n in range(fanout):
                pod_num = m * fanout + n
                for i in range(fanout):
                    for j in range(fanout):
                        p2_portid = j + 11
                        p2_nodeid = pod_num * fanout + i + 2000
                        p1_nodeid = i * fanout + j + 1000
                        p1_portid = pod_num + 1
                        p1 = (p1_nodeid,p1_portid)
                        p2 = (p2_nodeid,p2_portid)
                        l = Link(p1, p2)
                        self.links[p1]= l
                        self.nodes[p1_nodeid].ports[p1_portid] = l
                        l = Link(p2, p1)
                        self.links[p2]= l
                        self.nodes[p2_nodeid].ports[p2_portid] = l

        for m in range(fanout):
            for n in range(fanout):
                pod_num = m * fanout + n
                for i in range(fanout):
                    for j in range(fanout):
                        p2_portid = j + 1
                        p2_nodeid = pod_num * fanout + i + 2000
                        p3_nodeid = pod_num * fanout + j + 3000
                        p3_portid = i + 11
                        p2 = (p2_nodeid, p2_portid)
                        p3 = (p3_nodeid, p3_portid)
                        l = Link(p3, p2)
                        self.links[p3] = l
                        self.nodes[p3_nodeid].ports[p3_portid] = l
                        l = Link(p2, p3)
                        self.links[p2] = l
                        self.nodes[p2_nodeid].ports[p2_portid] = l

        for link in self.links.values():
            link.bandwidth = BANDWIDTH # Mbps
            link.speed = 0.0 # Mbps
            link.per_flow_speed = 0.0 # Mbps
            link.flows = []
            link.tx_bytes = 0
            link.rx_bytes = 0
            link.duration = 0

        for i in range(fanout**2):
            for j in range(fanout):
                nodeid = 3000+i*fanout+j
                self.hosts[nodeid]=((nodeid, 1),"10.%d.0.%d"%(i+1,j+1))
                self.nodes[nodeid].ports[1] = None

    def __flow_init(self):
        for i in self.hosts:
            for j in self.hosts:
                if i != j:
                    self.unused_host_pairs.append((i,j))
        self.unused_host_expect_len = len(self.unused_host_pairs) // 3
        self.__flow_gen()

    def __stats_update(self):
        for pair,flow in self.flows.items():
            if flow.path is None:
                flow.path = self.installed_path.get(pair)
                if flow.path is None:
                    paths = self.shortest_paths(*pair)
                    flow.path = paths[0] # random.randint(0,len(paths)//2-1)   hash(pair)%len(paths)
        for link in self.links.itervalues():
            link.flows = []
        for flow in self.flows.itervalues():
            if flow.total_size > 1000/1000000.0:
                path = flow.path
                for i in range(len(path)-1):
                    link = self.node_to_link(path[i],path[i+1])
                    link.flows.append(flow)
        for link in self.links.itervalues():
            n = len(link.flows)
            if n > 0:
                link.per_flow_speed = link.bandwidth * (1- random.random()*0.1) / n
            else:
                link.per_flow_speed = link.bandwidth * (1- random.random()*0.1)
        for flow in self.flows.itervalues():
            path = flow.path
            link_speed_list = []
            for i in range(len(path) - 1):
                link = self.node_to_link(path[i], path[i + 1])
                link_speed_list.append(link.per_flow_speed)
            flow.speed = min(link_speed_list)
        for link in self.links.itervalues():
            link.speed = sum(f.speed for f in link.flows)

    def __flow_run(self,time):
        for pair,flow in self.flows.items():
            run_size =flow.speed * time
            if run_size >= flow.remained_size:
                self.flows.pop(pair)
                self.unused_host_pairs.append(pair)
                run_size = flow.remained_size
                self.flowdonecount+=1
                self.traffic_size+=flow.total_size
            else:
                flow.remained_size -= run_size
            for i in range(len(flow.path)-1):
                src_node = flow.path[i]
                dst_node = flow.path[i+1]
                l = self.node_to_link(src_node,dst_node)
                self.links[l.start].tx_bytes = run_size*1000000/8
                self.links[l.end].rx_bytes = run_size*1000000/8
        for link in self.links.itervalues():
            link.duration+=time

    def show(self):
        print("flowdonecount=%d"%self.flowdonecount)
        print("traffic_size=%d"%self.traffic_size)

    def __flow_gen(self):
        l = len(self.unused_host_pairs) - self.unused_host_expect_len
        if l>0:
            random.shuffle(self.unused_host_pairs)
            for pair in self.unused_host_pairs[0:l]:
                self.flows[pair] = FlowStat(10*random.random())
            self.unused_host_pairs = self.unused_host_pairs[l:]
        print("generate %d flows"%l)

    def update(self, time=0.1):
        """
        update a time step, default time 0.1s
        :param time: time step length
        """
        self.__manage_op_list()
        self.__flow_run(time)
        self.__flow_gen()
        self.__stats_update()

    def shortest_paths(self, src_node, dst_node):
        """
        compute ECMP paths
        :return: ECMP paths
        """
        assert src_node>=3000 and dst_node>=3000 and src_node!=dst_node, "error range"
        fanout = self.fanout
        src_pod = (src_node-3000)//fanout
        dst_pod = (dst_node-3000)//fanout
        if src_pod == dst_pod:
            paths = [(src_node, 2000 + src_pod*fanout+i, dst_node) for i in range(fanout)]
            return paths
        else:
            paths = []
            for i in range(fanout):
                aggr_up = src_pod*fanout + i + 2000
                aggr_down = dst_pod*fanout + i + 2000
                for j in range(fanout):
                    core = i*fanout + j + 1000
                    paths.append((src_node, aggr_up, core, aggr_down, dst_node))
            return paths

    def node_to_link(self, src_node, dst_node):
        return self.links[self.node_to_port(src_node,dst_node)]

    def node_to_port(self, src_node, dst_node):
        src_layer = src_node//1000
        dst_layer = dst_node//1000
        if src_layer==1 and dst_layer==2:
            return self.__link_layer_1_2(src_node,dst_node)[0]
        elif src_layer==2 and dst_layer==1:
            return self.__link_layer_1_2(dst_node,src_node)[1]
        elif src_layer==2 and dst_layer==3:
            return self.__link_layer_2_3(src_node,dst_node)[0]
        elif src_layer==3 and dst_layer==2:
            return self.__link_layer_2_3(dst_node,src_node)[1]
        raise Exception("error")


    def __link_layer_1_2(self, start_node, end_node):
        """
        return start_port, end_port
        """
        assert 1000 <= start_node < 2000, "error range"
        assert 2000 <= end_node < 3000, "error range"
        end_pod = (end_node - 2000) // self.fanout
        end_n = (end_node-2000)%self.fanout
        start_n = start_node - 1000
        assert start_n//self.fanout == end_n,"error range"
        start_portno = end_pod + 1
        end_portno = start_n%self.fanout + 11
        start_port = (start_node, start_portno)
        end_port = (end_node,end_portno)
        assert self.links[start_port].end == end_port
        assert self.links[end_port].end == start_port
        return start_port,end_port

    def __link_layer_2_3(self, start_node, end_node):
        """
        return start_port, end_port
        """
        assert 2000 <= start_node < 3000, "error range"
        assert 3000 <= end_node < 4000, "error range"
        end_pod = (end_node - 3000) // self.fanout
        end_n = (end_node-3000)%self.fanout
        start_pod = (start_node - 2000) // self.fanout
        start_n = (start_node-2000)%self.fanout
        assert start_pod==end_pod, "errer range"
        start_port = (start_node, end_n+1)
        end_port = (end_node, start_n + 11)
        assert self.links[start_port].end == end_port
        assert self.links[end_port].end == start_port
        return start_port,end_port

    class PortDesc:
        pass
    class Port_Stat:
        pass

    def mock_port_desc(self,id):
        ev=Event()
        ev.dpid = id
        ev.body = []
        for portno,link in self.nodes[id].ports.iteritems():
            p = self.PortDesc()
            p.curr_speed = None # not used
            p.port_no=portno
            ev.body.append(p)
        return ev

    def mock_port_stats(self,id):
        ev=Event()
        ev.dpid = id
        ev.body = []
        for portno,link in self.nodes[id].ports.iteritems():
            p = self.Port_Stat()
            p.port_no = portno
            if link!=None:
                p.tx_bytes = link.tx_bytes
                p.rx_bytes = link.rx_bytes
                p.rx_errors = 0
                p.duration_sec = link.duration
                p.duration_nsec = 0
                ev.body.append(p)
            else:
                pass  #TODO maybe error
        return ev

    def mock_flow_stats(self,id):
        ev = Event()
        ev.dpid = id
        ev.body = []
        raise RuntimeError()

    def mock_flows_to_schedule(self):
        flows = []
        hostsList = []
        for pair,flow in self.flows.iteritems():
            flowDemand = flow.speed / BANDWIDTH
            if flow.total_size-flow.remained_size> 5:
                src = pair[0]
                dst = pair[1]
                if src not in hostsList:
                    hostsList.append(src)
                if dst not in hostsList:
                    hostsList.append(dst)
                flows.append({'src': src, 'dst': dst, 'demand': flowDemand,
                          'converged': False, 'receiver_limited': False,
                          'match': pair, 'priority': 5})
        return flows,hostsList

class Awareness:

    def __init__(self, fattree):
        self.fattree = fattree

    def shortest_paths(self, src_dp, dst_dp):
        return self.fattree.shortest_paths(src_dp,dst_dp)

    def link_to_port(self, src_dp, dst_dp):
        link = self.fattree.node_to_link(src_dp,dst_dp)
        return link.start,link.end



